{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Configuration\n",
    "\n",
    "Define settings for remote services, create `appsettings.Development.json` using `appsettings.json` as template.\n",
    "\n",
    "You need to manually give yourself `Cognitive Services OpenAI Contributor` rights on the `Azure OpenAI` resource if you not already have it.\n",
    "\n",
    "Also ensure to execute `azd auth login` to have access to the Azure resources.\n",
    "\n",
    "> This example is based on [How-To: Coordinate Agent Collaboration using Agent Group Chat](https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent/examples/example-agent-collaboration?pivots=programming-language-csharp).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Install and Import Required Packages\n",
    "Install and import the necessary packages using NuGet."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "// Install the necessary packages using NuGet\n",
    "#r \"nuget: Azure.Identity, 1.13.1\"\n",
    "// Only possible with .NET 9 support\n",
    "// #r \"nuget: Microsoft.Extensions.Logging, 9.0.0\"\n",
    "// #r \"nuget: Microsoft.Extensions.Logging.Console, 9.0.0\"\n",
    "#r \"nuget: Microsoft.Extensions.Configuration, 9.0.0\"\n",
    "#r \"nuget: Microsoft.Extensions.Configuration.Binder, 9.0.0\"\n",
    "#r \"nuget: Microsoft.Extensions.Configuration.UserSecrets, 9.0.0\"\n",
    "#r \"nuget: Microsoft.Extensions.Configuration.EnvironmentVariables, 9.0.0\"\n",
    "#r \"nuget: Microsoft.SemanticKernel, 1.33.0\"\n",
    "#r \"nuget: Microsoft.SemanticKernel.Agents.Core, 1.33.0-alpha\"\n",
    "#r \"nuget: Microsoft.SemanticKernel.Connectors.AzureOpenAI, 1.33.0\"\n",
    "\n",
    "// Import the necessary libraries\n",
    "using System;\n",
    "using System.Net;\n",
    "using System.ComponentModel;\n",
    "using System.Diagnostics;\n",
    "using System.IO;\n",
    "using System.Text.Json;\n",
    "using System.Threading.Tasks;\n",
    "using Azure.Identity;\n",
    "using Microsoft.DotNet.Interactive;\n",
    "// Only possible with .NET 9 support\n",
    "// using Microsoft.Extensions.Logging;\n",
    "using Microsoft.Extensions.Configuration;\n",
    "using Microsoft.Extensions.DependencyInjection;\n",
    "using Microsoft.SemanticKernel;\n",
    "using Microsoft.SemanticKernel.Agents;\n",
    "using Microsoft.SemanticKernel.Agents.Chat;\n",
    "using Microsoft.SemanticKernel.Agents.History;\n",
    "using Microsoft.SemanticKernel.ChatCompletion;\n",
    "using Microsoft.SemanticKernel.Connectors.AzureOpenAI;"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Create Kernel Builder\n",
    "Create a Kernel builder instance."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "var configBuilder = new ConfigurationBuilder()\n",
    "    .SetBasePath(Directory.GetCurrentDirectory())\n",
    "    .AddJsonFile(\"./appsettings.json\", optional: false)\n",
    "    .AddJsonFile(\"./appsettings.Development.json\", optional: false);\n",
    "var configuration = configBuilder.Build();\n",
    "\n",
    "IKernelBuilder builder = Microsoft.SemanticKernel.Kernel\n",
    "    .CreateBuilder()\n",
    "    .AddAzureOpenAIChatCompletion(\n",
    "        configuration[\"ChatModelDeployment\"],\n",
    "        configuration[\"Endpoint\"],\n",
    "        new AzureDeveloperCliCredential());\n",
    "\n",
    "// var loggerFactory = LoggerFactory.Create(builder =>\n",
    "// {\n",
    "//     builder.AddSimpleConsole(options =>\n",
    "//     {\n",
    "//         options.IncludeScopes = true;\n",
    "//         options.SingleLine = true;\n",
    "//         options.TimestampFormat = \"HH:mm:ss \";\n",
    "//     });\n",
    "// });\n",
    "\n",
    "// builder.Services.AddSingleton(loggerFactory);\n",
    "\n",
    "private sealed class FunctionInvocationFilter() : IFunctionInvocationFilter\n",
    "{\n",
    "    public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func<FunctionInvocationContext, Task> next)\n",
    "    {\n",
    "        if (context.Function.PluginName == \"SearchPlugin\")\n",
    "        {\n",
    "            Console.WriteLine($\"{context.Function.Name}:{JsonSerializer.Serialize(context.Arguments)}\");\n",
    "        }\n",
    "        await next(context);\n",
    "    }\n",
    "}\n",
    "\n",
    "builder.Services.AddSingleton<IFunctionInvocationFilter, FunctionInvocationFilter>();\n",
    "\n",
    "Microsoft.SemanticKernel.Kernel kernel = builder.Build();\n",
    "\n",
    "public class Tools\n",
    "{\n",
    "    [KernelFunction]\n",
    "    [Description(\"Copies the provided content to the console output.\")]\n",
    "    public static void WriteConsoleOutput(string content)\n",
    "    {\n",
    "        if (string.IsNullOrWhiteSpace(content))\n",
    "        {\n",
    "            return;\n",
    "        }\n",
    "\n",
    "        Console.Write(content);\n",
    "    }\n",
    "}\n",
    "\n",
    "Microsoft.SemanticKernel.Kernel toolKernel = kernel.Clone();\n",
    "toolKernel.Plugins.AddFromType<Tools>()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "# Agent Definitions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "// The Agent Framework is experimental and requires warning suppression\n",
    "#pragma warning disable CA2007, IDE1006, SKEXP0001, SKEXP0110, OPENAI001\n",
    "\n",
    "const string ReviewerName = \"Reviewer\";\n",
    "const string WriterName = \"Writer\";\n",
    "\n",
    "ChatCompletionAgent agentReviewer =\n",
    "    new()\n",
    "    {\n",
    "        Name = ReviewerName,\n",
    "        Instructions =\n",
    "            \"\"\"\n",
    "            Your responsiblity is to review and identify how to improve user provided content.\n",
    "            If the user has providing input or direction for content already provided, specify how to address this input.\n",
    "            Never directly perform the correction or provide example.\n",
    "            Once the content has been updated in a subsequent response, you will review the content again until satisfactory.\n",
    "            Always copy satisfactory content to the console output using available tools and inform user.\n",
    "\n",
    "            RULES:\n",
    "            - Only identify suggestions that are specific and actionable.\n",
    "            - Verify previous suggestions have been addressed.\n",
    "            - Never repeat previous suggestions.\n",
    "            \"\"\",\n",
    "        Kernel = toolKernel,\n",
    "        Arguments =\n",
    "            new KernelArguments(\n",
    "                new AzureOpenAIPromptExecutionSettings() \n",
    "                { \n",
    "                    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() \n",
    "                })\n",
    "    };\n",
    "\n",
    "ChatCompletionAgent agentWriter =\n",
    "    new()\n",
    "    {\n",
    "        Name = WriterName,\n",
    "        Instructions =\n",
    "            \"\"\"\n",
    "            Your sole responsiblity is to rewrite content according to review suggestions.\n",
    "\n",
    "            - Always apply all review direction.\n",
    "            - Always revise the content in its entirety without explanation.\n",
    "            - Never address the user.\n",
    "            \"\"\",\n",
    "        Kernel = kernel,\n",
    "    };"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Chat Definitions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "// The Agent Framework is experimental and requires warning suppression\n",
    "#pragma warning disable CA2007, IDE1006, SKEXP0001, SKEXP0110, OPENAI001\n",
    "\n",
    "KernelFunction selectionFunction =\n",
    "    AgentGroupChat.CreatePromptFunctionForStrategy(\n",
    "        $$$\"\"\"\n",
    "        Examine the provided RESPONSE and choose the next participant.\n",
    "        State only the name of the chosen participant without explanation.\n",
    "        Never choose the participant named in the RESPONSE.\n",
    "\n",
    "        Choose only from these participants:\n",
    "        - {{{ReviewerName}}}\n",
    "        - {{{WriterName}}}\n",
    "\n",
    "        Always follow these rules when choosing the next participant:\n",
    "        - If RESPONSE is user input, it is {{{ReviewerName}}}'s turn.\n",
    "        - If RESPONSE is by {{{ReviewerName}}}, it is {{{WriterName}}}'s turn.\n",
    "        - If RESPONSE is by {{{WriterName}}}, it is {{{ReviewerName}}}'s turn.\n",
    "\n",
    "        RESPONSE:\n",
    "        {{$lastmessage}}\n",
    "        \"\"\",\n",
    "        safeParameterNames: \"lastmessage\");\n",
    "\n",
    "\n",
    "const string TerminationToken = \"yes\";\n",
    "\n",
    "KernelFunction terminationFunction =\n",
    "    AgentGroupChat.CreatePromptFunctionForStrategy(\n",
    "        $$$\"\"\"\n",
    "        Examine the RESPONSE and determine whether the content has been deemed satisfactory.\n",
    "        If content is satisfactory, respond with a single word without explanation: {{{TerminationToken}}}.\n",
    "        If specific suggestions are being provided, it is not satisfactory.\n",
    "        If no correction is suggested, it is satisfactory.\n",
    "\n",
    "        RESPONSE:\n",
    "        {{$lastmessage}}\n",
    "        \"\"\",\n",
    "        safeParameterNames: \"lastmessage\");        "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Creating Group Chat"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "// The Agent Framework is experimental and requires warning suppression\n",
    "#pragma warning disable CA2007, IDE1006, SKEXP0001, SKEXP0110, OPENAI001\n",
    "\n",
    "ChatHistoryTruncationReducer historyReducer = new(1);\n",
    "\n",
    "AgentGroupChat chat =\n",
    "    new(agentReviewer, agentWriter)\n",
    "    {\n",
    "        ExecutionSettings = new AgentGroupChatSettings\n",
    "        {\n",
    "            SelectionStrategy =\n",
    "                new KernelFunctionSelectionStrategy(selectionFunction, kernel)\n",
    "                {\n",
    "                    // Always start with the editor agent.\n",
    "                    InitialAgent = agentReviewer,\n",
    "                    // Save tokens by only including the final response\n",
    "                    HistoryReducer = historyReducer,\n",
    "                    // The prompt variable name for the history argument.\n",
    "                    HistoryVariableName = \"lastmessage\",\n",
    "                    // Returns the entire result value as a string.\n",
    "                    ResultParser = (result) => result.GetValue<string>() ?? agentReviewer.Name\n",
    "                },\n",
    "            TerminationStrategy =\n",
    "                new KernelFunctionTerminationStrategy(terminationFunction, kernel)\n",
    "                {\n",
    "                    // Only evaluate for editor's response\n",
    "                    Agents = [agentReviewer],\n",
    "                    // Save tokens by only including the final response\n",
    "                    HistoryReducer = historyReducer,\n",
    "                    // The prompt variable name for the history argument.\n",
    "                    HistoryVariableName = \"lastmessage\",\n",
    "                    // Limit total number of turns\n",
    "                    MaximumIterations = 12,\n",
    "                    // Customer result parser to determine if the response is \"yes\"\n",
    "                    ResultParser = (result) => result.GetValue<string>()?.Contains(TerminationToken, StringComparison.OrdinalIgnoreCase) ?? false\n",
    "                }\n",
    "        }\n",
    "    };\n",
    "\n",
    "Console.WriteLine(\"Ready!\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Chat Loop for Text Review\n",
    "\n",
    "Execute it and enter a text for review.\n",
    "If no text is entered, a sample will be used.\n",
    "\n",
    "**Commands**\n",
    "- `@<file path>` - using a text file as input\n",
    "- `RESET` - reset everything and start again\n",
    "- `EXIT` - exit chat\n",
    "- (blank) - use sampe text for review\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "dotnet_interactive": {
     "language": "csharp"
    },
    "polyglot_notebook": {
     "kernelName": "csharp"
    },
    "vscode": {
     "languageId": "polyglot-notebook"
    }
   },
   "outputs": [],
   "source": [
    "// The Agent Framework is experimental and requires warning suppression\n",
    "#pragma warning disable CA2007, IDE1006, SKEXP0001, SKEXP0110, OPENAI001\n",
    "\n",
    "var sampleTextForReview = @\"Elephants are the largest land mamma4ls on earth and have distinctly massive bodies, large ears, and long trunks.\n",
    "    They use their trunks to pick up objects, trumpet warnings, greet other elephants, or suck up water for drinking or bathing, among other uses.\n",
    "    Both male and female African elephants grow tusks and each individual can either be left- or right-tusked, an5d the one they use more is usually\n",
    "    smaller because of wear and tear. Elephant tusks serve many purposes. \n",
    "    These extended teeth can be used to protect the elephant's ztrunk, lift and move objects, gather food, and strip bark from trees. \n",
    "    They can also be used for defense. During times of droughdd, elephants even use their tusks to dig holes to find water underground.\";\n",
    "\n",
    "bool isComplete = false;\n",
    "do\n",
    "{\n",
    "    Console.WriteLine();\n",
    "    Console.Write(\"> \");\n",
    "    var input = await Microsoft.DotNet.Interactive.Kernel.GetInputAsync(\"Chat (insert text for review or ' '[space] for sample):\");\n",
    "    if (string.IsNullOrWhiteSpace(input))\n",
    "    {\n",
    "        Console.WriteLine(\"[Using sample]\");\n",
    "        input = sampleTextForReview;\n",
    "    }\n",
    "    input = input.Trim();\n",
    "    if (input.Equals(\"EXIT\", StringComparison.OrdinalIgnoreCase))\n",
    "    {\n",
    "        isComplete = true;\n",
    "        break;\n",
    "    }\n",
    "\n",
    "    if (input.Equals(\"RESET\", StringComparison.OrdinalIgnoreCase))\n",
    "    {\n",
    "        await chat.ResetAsync();\n",
    "        Console.WriteLine(\"[Converation has been reset]\");\n",
    "        continue;\n",
    "    }\n",
    "\n",
    "    if (input.StartsWith(\"@\", StringComparison.Ordinal) && input.Length > 1)\n",
    "    {\n",
    "        string filePath = input.Substring(1);\n",
    "        try\n",
    "        {\n",
    "            if (!File.Exists(filePath))\n",
    "            {\n",
    "                Console.WriteLine($\"Unable to access file: {filePath}\");\n",
    "                continue;\n",
    "            }\n",
    "            input = File.ReadAllText(filePath);\n",
    "        }\n",
    "        catch (Exception)\n",
    "        {\n",
    "            Console.WriteLine($\"Unable to access file: {filePath}\");\n",
    "            continue;\n",
    "        }\n",
    "    }\n",
    "\n",
    "    Console.WriteLine();\n",
    "    Console.WriteLine($\"User: {input}\");\n",
    "    chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));\n",
    "\n",
    "    chat.IsComplete = false;\n",
    "\n",
    "    try\n",
    "    {\n",
    "        await foreach (ChatMessageContent response in chat.InvokeAsync())\n",
    "        {\n",
    "            Console.WriteLine();\n",
    "            Console.WriteLine($\"{response.AuthorName.ToUpperInvariant()}:{Environment.NewLine}{response.Content}\");\n",
    "        }\n",
    "    }\n",
    "    catch (HttpOperationException exception)\n",
    "    {\n",
    "        Console.WriteLine(exception.Message);\n",
    "        if (exception.InnerException != null)\n",
    "        {\n",
    "            Console.WriteLine(exception.InnerException.Message);\n",
    "            if (exception.InnerException.Data.Count > 0)\n",
    "            {\n",
    "                Console.WriteLine(JsonSerializer.Serialize(exception.InnerException.Data, new JsonSerializerOptions() { WriteIndented = true }));\n",
    "            }\n",
    "        }\n",
    "    }\n",
    "} while (!isComplete);"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".NET (C#)",
   "language": "C#",
   "name": ".net-csharp"
  },
  "language_info": {
   "name": "python"
  },
  "polyglot_notebook": {
   "kernelInfo": {
    "defaultKernelName": "csharp",
    "items": [
     {
      "aliases": [],
      "name": "csharp"
     }
    ]
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
